"use client"; import { use, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft, Save, AlertTriangle, Pencil, X, Copy, Check, BookMarked, Hash, Clock, FileText, } from "lucide-react"; import { PageHeader } from "@/components/common/page-header"; import { EmptyState } from "@/components/common/empty-state"; import { DeleteLearningDangerZone } from "@/components/common/delete-learning-danger-zone"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { reflexio } from "@/lib/reflexio-client"; import { formatTimestamp, truncateId } from "@/lib/format"; import { cn } from "@/lib/utils"; import { agentPlaybookStatusLabel, statusLabel } from "@/lib/status"; import type { StatusLabel } from "@/lib/status"; import type { AgentPlaybook, AgentPlaybookStatus } from "@/lib/types"; type FormState = { content: string; trigger: string; rationale: string; playbookStatus: AgentPlaybookStatus; }; function toForm(p: AgentPlaybook): FormState { return { content: p.content, trigger: p.trigger ?? "", rationale: p.rationale ?? "", playbookStatus: p.playbook_status, }; } function displayName(name: string | null | undefined): string | null { if (!name) return null; if (name === "default_playbook_extractor") return "shared skill"; return name; } const REVIEW_STATUS_META: Record< AgentPlaybookStatus, { label: string; description: string } > = { pending: { label: "Auto generated", description: "Auto-generated shared skill. It may be updated automatically.", }, approved: { label: "Persisted", description: "Persisted shared skill. It will not be auto updated.", }, rejected: { label: "Rejected", description: "Rejected shared skill. It will not be used in claude-smart.", }, }; export default function SharedSkillDetailPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = use(params); const router = useRouter(); const [playbook, setPlaybook] = useState(null); const [notFound, setNotFound] = useState(false); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [reviewingStatus, setReviewingStatus] = useState(null); const [editing, setEditing] = useState(false); const [form, setForm] = useState({ content: "", trigger: "", rationale: "", playbookStatus: "pending", }); useEffect(() => { let cancelled = false; reflexio .getAgentPlaybooks({}) .then((res) => { if (cancelled) return; const found = (res.agent_playbooks ?? []).find( (p) => String(p.agent_playbook_id) === id, ); if (!found) { setNotFound(true); return; } setPlaybook(found); setForm(toForm(found)); }) .catch((e) => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }); return () => { cancelled = true; }; }, [id]); const dirty = useMemo(() => { if (!playbook) return false; const orig = toForm(playbook); return ( orig.content !== form.content || orig.trigger !== form.trigger || orig.rationale !== form.rationale || orig.playbookStatus !== form.playbookStatus ); }, [playbook, form]); const save = async () => { if (!playbook || !dirty) return; setSaving(true); setError(null); try { await reflexio.updateAgentPlaybook( { agent_playbook_id: playbook.agent_playbook_id, content: form.content, trigger: form.trigger || null, rationale: form.rationale || null, playbook_status: form.playbookStatus, }, ); setPlaybook({ ...playbook, content: form.content, trigger: form.trigger || null, rationale: form.rationale || null, playbook_status: form.playbookStatus, }); setEditing(false); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setSaving(false); } }; const setReviewStatus = async (nextStatus: AgentPlaybookStatus) => { if (!playbook || playbook.playbook_status === nextStatus) return; if ( nextStatus === "rejected" && !confirm( `Reject shared skill #${playbook.agent_playbook_id}? Rejected shared skills will not be used in claude-smart.`, ) ) { return; } setReviewingStatus(nextStatus); setError(null); try { await reflexio.updateAgentPlaybook( { agent_playbook_id: playbook.agent_playbook_id, playbook_status: nextStatus, }, ); setPlaybook((current) => current ? { ...current, playbook_status: nextStatus } : current, ); setForm((current) => ({ ...current, playbookStatus: nextStatus })); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setReviewingStatus(null); } }; const remove = async () => { if (!playbook) return; setDeleting(true); try { await reflexio.deleteAgentPlaybook(playbook.agent_playbook_id); router.push("/skills"); } catch (e) { setError(e instanceof Error ? e.message : String(e)); setDeleting(false); } }; const cancelEdit = () => { if (playbook) setForm(toForm(playbook)); setEditing(false); }; if (notFound) { return (
} />
); } const lifecycleStatus = playbook ? statusLabel(playbook) : null; const playbookStatus = playbook ? agentPlaybookStatusLabel(playbook) : null; return (
{!editing ? ( ) : ( <> )}
} />
{error && (
{error}
)} {playbook && (
{editing ? ( ) : ( )} {lifecycleStatus !== "CURRENT" && ( )} {displayName(playbook.playbook_name) && ( {displayName(playbook.playbook_name)} )} {dirty && ( unsaved changes )}
)}
{editing ? ( setForm((f) => ({ ...f, trigger: v }))} rows={2} placeholder="e.g. When writing or running async Python tests." /> ) : ( )}
{editing ? ( ) : ( playbook && ( ) )}
{editing ? ( setForm((f) => ({ ...f, content: v }))} rows={6} placeholder="e.g. Use anyio with trio backend — never pytest-asyncio." /> ) : ( )}
{editing ? ( setForm((f) => ({ ...f, rationale: v }))} rows={3} placeholder="e.g. pytest-asyncio deadlocked CI on project X — trio is the project standard." /> ) : ( )}
{!editing && playbook && ( <> )}
{playbook && ( )}
); } function Section({ icon: Icon, title, hint, children, }: { icon: React.ComponentType<{ className?: string }>; title: string; hint?: string; children: React.ReactNode; }) { return (
{hint && ( {hint} )}
{children}
); } function Prose({ text, muted = false }: { text: string; muted?: boolean }) { if (!text) { return (

{muted ? "Not set" : "—"}

); } return (

{text}

); } function AutoTextarea({ value, onChange, rows = 3, placeholder, }: { value: string; onChange: (v: string) => void; rows?: number; placeholder?: string; }) { return (